﻿//-----------------------------------------------------------------------
// <copyright file="IMembershipServiceFeaturesExtensions.cs" company="OOHM PROCESSAMENTO DE DADOS LTDA.">
//     Microsoft Public License (MS-PL) This license governs use of the accompanying
//     software. If you use the software, you accept this license. If you do not
//     accept the license, do not use the software. 1. Definitions The terms "reproduce,"
//     "reproduction," "derivative works," and "distribution" have the same meaning
//     here as under U.S. copyright law. A "contribution" is the original software,
//     or any additions or changes to the software. A "contributor" is any person
//     that distributes its contribution under this license. "Licensed patents"
//     are a contributor's patent claims that read directly on its contribution.
//     2. Grant of Rights (A) Copyright Grant- Subject to the terms of this license,
//     including the license conditions and limitations in section 3, each contributor
//     grants you a non-exclusive, worldwide, royalty-free copyright license to
//     reproduce its contribution, prepare derivative works of its contribution,
//     and distribute its contribution or any derivative works that you create.
//     (B) Patent Grant- Subject to the terms of this license, including the license
//     conditions and limitations in section 3, each contributor grants you a non-exclusive,
//     worldwide, royalty-free license under its licensed patents to make, have
//     made, use, sell, offer for sale, import, and/or otherwise dispose of its
//     contribution in the software or derivative works of the contribution in
//     the software. 3. Conditions and Limitations (A) No Trademark License- This
//     license does not grant you rights to use any contributors' name, logo, or
//     trademarks. (B) If you bring a patent claim against any contributor over
//     patents that you claim are infringed by the software, your patent license
//     from such contributor to the software ends automatically. (C) If you distribute
//     any portion of the software, you must retain all copyright, patent, trademark,
//     and attribution notices that are present in the software. (D) If you distribute
//     any portion of the software in source code form, you may do so only under
//     this license by including a complete copy of this license with your distribution.
//     If you distribute any portion of the software in compiled or object code
//     form, you may only do so under a license that complies with this license.
//     (E) The software is licensed "as-is." You bear the risk of using it. The
//     contributors give no express warranties, guarantees or conditions. You may
//     have additional consumer rights under your local laws which this license
//     cannot change. To the extent permitted under your local laws, the contributors
//     exclude the implied warranties of merchantability, fitness for a particular
// </copyright>
//-----------------------------------------------------------------------
namespace AbstractSaaSTemplate.UI.Web.Helpers{    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using AbstractSaaSTemplate.Application.Mvc;
    using AbstractSaaSTemplate.Domain.Models.Entities;
    using AbstractSaaSTemplate.Domain.Models.Enumerators;
    using AbstractSaaSTemplate.Domain.Repositories;
    using AbstractSaaSTemplate.Domain.Services;
    using AbstractSaaSTemplate.UI.Web.Filters.Actions.Security;

    /// <summary>
    /// Extends the IMembershipService with the possibility of retrieving
    /// this is done as an extension because the Infra-structure does not have
    /// access to the UI so it cannot implement it. Also, the IMembershipRepository
    /// is used whenever we want to find out about access authorisation which
    /// just makes it contextual to it. Additionally, once we are extending this
    /// interface, we can use its properties and methods to do some extras, such
    /// as checking which among the features are authorised for that user.
    /// </summary>
    public static class IMembershipServiceFeaturesExtensions
    {
        /// <summary>
        /// Keeps cached access definitions.
        /// </summary>
        private static readonly IDictionary<ControlledFeatures, ControlledFeatureInfo> accessDefinitions = new Dictionary<ControlledFeatures, ControlledFeatureInfo>();

        /// <summary>
        /// Provides a cached list with all controlled features in the
        /// system marked by the FeatureAuthorizationAttribute.
        /// </summary>
        /// <param name="serviceForMemberships">The service for memberships that will be extended.</param>
        /// <returns>Returns an enumerable of features</returns>
        public static IEnumerable<ControlledFeatureInfo> GetAllFeatures(
            this IMembershipService serviceForMemberships)
        {
            // checks if the serivce exists
            if (serviceForMemberships == null)
            {
                return null;
            }
            
            InitializeDefinitions();

            return accessDefinitions.Values;
        }

        /// <summary>
        /// Provides a cached list with all controlled features available
        /// for a particular access level from all methods in the system marked
        /// by the FeatureAuthorizationAttribute.
        /// </summary>
        /// <param name="serviceForMemberships">The service for memberships that will be extended.</param>
        /// <param name="level">The access level for which we are querying.</param>
        /// <returns>Returns an enumerable of features.</returns>
        public static IEnumerable<ControlledFeatureInfo> GetAllFeaturesForLevel(
            this IMembershipService serviceForMemberships,
            AccessLevels level)
        {
            // checks if the serivce exists
            if (serviceForMemberships == null)
            {
                return null;
            }

            InitializeDefinitions();

            var authorisedFeatures = new List<ControlledFeatureInfo>();
            foreach (var accessInfo in accessDefinitions)
            {
                // we first check the access levels.
                // if the levesl required by the feature are higher than the user, we continue
                if ((accessInfo.Value.Levels & level) != level)
                {
                    continue;
                }

                authorisedFeatures.Add(accessInfo.Value);
            }

            return authorisedFeatures;
        }
        
        /// <summary>
        /// Provides a cached list with all controlled features in the
        /// accesible for the role from all methods in the system marked
        /// by the FeatureAuthorizationAttribute.
        /// </summary>
        /// <param name="serviceForMemberships">The service for memberships that will be extended.</param>
        /// <param name="repositoryForMemberships">The repository to operate 'Memberships'.</param>
        /// <param name="user">The user for which access will be checked.</param>
        /// <returns>Returns an enumerable of features</returns>
        public static IEnumerable<ControlledFeatureInfo> GetAllFeaturesForUser(
            this IMembershipService serviceForMemberships,
            IMembershipRepository repositoryForMemberships,
            User user)
        {
            // checks if the serivce exists
            if (serviceForMemberships == null)
            {
                return null;
            }

            if (repositoryForMemberships == null)
            {
                throw new ArgumentNullException("repositoryForMemberships");
            }

            if (user == null)
            {
                throw new ArgumentNullException("user");
            }

            InitializeDefinitions();

            var authorisedFeatures = new List<ControlledFeatureInfo>();
            foreach (var accessInfo in accessDefinitions)
            {
                // we first check the access levels.
                // if the levesl required by the feature are higher than the user, we continue
                if ((accessInfo.Value.Levels & user.AccessLevel) != user.AccessLevel)
                {
                    continue;
                }
                
                // we retrieve all roles associated with that user.
                var roles = repositoryForMemberships.GetRolesByCredentials(user.Credentials.Login);

                // and we test each role to find which one of them
                // should have access to a particular function if any
                // of the roles has access, we grant the user rights
                // to access it.
                foreach (var role in roles)
                {
                    var hasAccess = serviceForMemberships.HasRoleAccessToFeature(
                        accessInfo.Value.Levels,
                        role,
                        accessInfo.Key);

                    if (hasAccess)
                    {
                        authorisedFeatures.Add(accessInfo.Value);
                        break;
                    }
                }
            }

            return authorisedFeatures;
        }

        /// <summary>
        /// Initializes the access definitions.
        /// </summary>
        private static void InitializeDefinitions()
        {
            // if previously loaded and cached, we retrieve the
            // list of features available in the caching system.
            if (accessDefinitions.Count != 0)
            {
                return;
            }

            // by locking 'accessDefinitions' we can safely load the content
            // before anybody writes or retrieves from it, preventing problems
            // related to partial loading or thread concurrence.
            lock (accessDefinitions)
            {
                // otherwise, we will use the super-powers of the Reflection
                // namespace to find all the controller actions marked
                // with our FeatureAuthorizationAttribute. This will 
                // make the programmers life's way easier.
                var assembly = Assembly.GetExecutingAssembly();
                var parentType = typeof(ApplicationController);
                var attributeType = typeof(FeatureAuthorizationAttribute);
                var controllers = assembly.GetTypes()
                    .Where(type => parentType.IsAssignableFrom(type))
                    .ToArray();

                // now we will check each controller action to see if
                // we find the attribute that we are seeking for.
                foreach (var controller in controllers)
                {
                    // we get the controller name which will be used
                    // for routes down below.
                    var controllerName = controller.Name.Replace("Controller", string.Empty);

                    // the FeatureAuthorizationAttribute can decorate classes
                    // or methods. In any case we perform the same procedure
                    // to cache definitions for that feature.
                    var attributes = controller.GetCustomAttributes(attributeType, true);
                    if (attributes.Length != 0)
                    {
                        var attribute = attributes.FirstOrDefault() as FeatureAuthorizationAttribute;
                        if (attribute != null)
                        {
                            AddOrUpdateFeature(
                                attribute.Features,
                                attribute.Levels,
                                controllerName,
                                string.Empty,
                                false);
                        }
                    }

                    var methods = controller.GetMethods();
                    foreach (var method in methods)
                    {
                        // the FeatureAuthorizationAttribute can decorate methods
                        // or methods. In any case we perform the same procedure
                        // to cache definitions for that feature.
                        attributes = method.GetCustomAttributes(attributeType, true);
                        if (attributes.Length != 0)
                        {
                            var attribute = attributes.FirstOrDefault() as FeatureAuthorizationAttribute;
                            if (attribute != null)
                            {
                                var actionName = method.Name;
                                
                                AddOrUpdateFeature(
                                    attribute.Features,
                                    attribute.Levels,
                                    controllerName,
                                    actionName,
                                    attribute.LandingAction);
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Creates or Updates feature definitions.
        /// It trusts that the 'accessDefinitions' has been locked
        /// by the parent caller, noto doing any thread safe check.
        /// </summary>
        /// <param name="features">The feature which we want to work on.</param>
        /// <param name="levelsToRelate">The access level which will be added to the definition.</param>
        /// <param name="controllerName">The name of the controller for the route.</param>
        /// <param name="actionName">The name of the action for the route.</param>
        /// <param name="isLandingRoute">If this is the landing route, it may replace any previous controller name and action.</param>
        private static void AddOrUpdateFeature(
            ControlledFeatures features,
            AccessLevels levelsToRelate,
            string controllerName,
            string actionName,
            bool isLandingRoute)
        {
            var featuresAvailable = Enum.GetNames(typeof(ControlledFeatures));
            foreach (var availableFeature in featuresAvailable)
            {
                var feature = ControlledFeatures.None;
                if (!Enum.TryParse<ControlledFeatures>(availableFeature, out feature))
                {
                    continue;
                }

                if (feature == ControlledFeatures.None)
                {
                    continue;
                }

                if ((feature & features) != feature)
                {
                    continue;
                }

                if (!accessDefinitions.ContainsKey(feature))
                {
                    var newInfo = new ControlledFeatureInfo(
                        feature,
                        levelsToRelate,
                        controllerName,
                        actionName);

                    accessDefinitions.Add(feature, newInfo);
                }

                var accessInfo = accessDefinitions[feature];
                accessInfo.Levels = accessInfo.Levels | levelsToRelate;

                // If this is the landing route, it may replace any
                // previous controller name and action.
                if (isLandingRoute)
                {
                    accessInfo.ControllerName = controllerName;
                    accessInfo.ActionName = actionName;
                }
            }
        }
    }
}

